﻿/*
 * Copyright © 2020, Wuxi Safover Intelligent Technology Co., Ltd. All rights reserved.
 * 
 * 无锡安永智能科技有限公司版权所有，未经许可，不得用于商业用途
 * 
 * Visit: https://www.safover.com/ 
 */

using log4net;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Ports;
using System.Reflection;
using System.Threading;
using Safover.LidarCollector.Extensions;

namespace Safover.LidarCollector.Core
{
    public abstract class SerialPortDevice : ISerialPortControl
    {
        private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

        public static string ToHexString(IEnumerable<byte> bytes)
        {
            return PacketUtils.ToHexString(bytes);
        }

        private SerialPort SerialPort { get; set; }

        public abstract string DeviceType { get; }

        public string PortName { get; set; }

        public int BaudRate { get; set; }

        public int DataBits { get; set; } = 8;

        public Parity Parity { get; set; } = Parity.None;

        public StopBits StopBits { get; set; } = StopBits.One;

        public Handshake Handshake { get; set; } = Handshake.None;

        public int WriteTimeout { get; set; } = 1000;

        public int ReadTimeout { get; set; } = 1000;

        public SerialPortStatusChangedEventHandler SerialPortStatusChanged { get; set; }

        public DeviceReadingsUpdatedEventHandler DeviceReadingsUpdated { get; set; }


        #region Connection Management

        private bool autoKeepConnection;
        private bool AutoKeepConnection
        {
            get => autoKeepConnection;
            set
            {
                autoKeepConnection = value;

                Debug.WriteLine($"[{DeviceType}]: {DateTime.Now:HH:mm:ss}: [{Thread.CurrentThread.Name}] - try stop working.");
                ManageConnection(autoKeepConnection);
            }
        }

        private Thread ConnectionManagerThread { get; set; }

        private bool ConnectionManagerThreadIsRunning { get; set; }

        private void ManageConnection(bool keepConnected)
        {
            if (keepConnected)
            {
                if (!ConnectionManagerThreadIsRunning)
                {
                    ConnectionManagerThread = new Thread(ManageConnectionLoop)
                    {
                        Name = $"ConnectionManagerThread_{DeviceType}"
                    };

                    ConnectionManagerThread.Start();
                }
            }
            else if (ConnectionManagerThreadIsRunning)
            {
                ConnectionManagerThread.Join();
            }
        }

        private void ManageConnectionLoop()
        {
            if (ConnectionManagerThreadIsRunning)
            {
                return;
            }

            Debug.WriteLine($"[{DeviceType}]: [{Thread.CurrentThread.Name}] - started working.");

            ConnectionManagerThreadIsRunning = true;

            const int nextTryDelaySeconds = 10;

            try
            {
                while (AutoKeepConnection)
                {
                    if (IsSerialPortOpen)
                    {
                        Thread.Sleep(1000);
                    }
                    else
                    {
                        try
                        {
                            ResetSerialPortParameters();

                            OpenSerialPort();

                            SerialPortStatusChanged?.Invoke(this, new SerialPortStatusChangedEventArgs(DeviceType, true, "OK"));
                        }
                        catch (Exception ex)
                        {
                            SerialPortStatusChanged?.Invoke(this, new SerialPortStatusChangedEventArgs(DeviceType, false, ex.Message));
                            Log.Error($"[{DeviceType}]:  Failed to open serial port.", ex);
                        }

                        for (int i = 0; i < nextTryDelaySeconds; i++)
                        {
                            Thread.Sleep(1000);

                            if (!AutoKeepConnection)
                            {
                                return;
                            }
                        }
                    }
                }
            }
            finally 
            {
                Debug.WriteLine($"[{DeviceType}]: {DateTime.Now:HH:mm:ss} - [{Thread.CurrentThread.Name}] stopped working.");
                ConnectionManagerThreadIsRunning = false;
            }
        }

        #endregion

        protected int BytesToRead 
        {
            get 
            {
                if (SerialPort == null) return 0;

                return SerialPort.BytesToRead;
            }
        }

        abstract protected void OnDataReceived(object sender, SerialDataReceivedEventArgs e);

        protected virtual void ResetSerialPortParameters()
        { }

        public virtual bool IsSerialPortOpen => SerialPort != null && SerialPort.IsOpen;

        protected virtual void DoOpenSerialPort()
        {
            if (IsSerialPortOpen)
            {
                throw new InvalidOperationException($"[{DeviceType}]: Port has already been opened.");
            }

            SerialPort = new SerialPort
            {
                PortName = PortName,
                BaudRate = BaudRate,
                Parity = Parity,
                StopBits = StopBits,
                DataBits = DataBits,
                Handshake = Handshake,
                WriteTimeout = WriteTimeout,
                ReadTimeout = ReadTimeout,
            };

            SerialPort.DataReceived += OnDataReceived;

            Log.Debug($"[{DeviceType}] Ready to open: PortName={PortName}, BaudRate={BaudRate}, DataBits={DataBits}, Parity={Parity}, StopBits={StopBits}, Handshake={Handshake};");
            SerialPort.Open();
        }

        protected virtual bool DoCloseSerialPort()
        {
            if (SerialPort == null)
            {
                return false;
                //throw new InvalidOperationException($"Port already closed, device=[{DeviceType}].");
            }

            SerialPort.DataReceived -= OnDataReceived;
            Thread.Sleep(1000);
            SerialPort.Close();
            return true;
        }

        public void CloseSerialPort()
        {
            try
            {
                AutoKeepConnection = false;
                if (DoCloseSerialPort()) 
                {
                    SerialPortStatusChanged?.Invoke(this, new SerialPortStatusChangedEventArgs(DeviceType, false, "Device Shut Down."));
                }
            }
            catch (IOException e)
            {
                throw new SerialPortException($"[{DeviceType}]: The port is in an invalid state.", e);
            }
        }

        public void KeepOpenSerialPort()
        {
            AutoKeepConnection = true;
        }

        private void OpenSerialPort()
        {
            try
            {
                DoOpenSerialPort();
                Log.Info($"[{DeviceType}]: port is openned. ");
            }
            catch (UnauthorizedAccessException e)
            {
                throw new SerialPortException($"[{DeviceType}]: Access denied or the serial port is ocupied. ", e);
            }
            catch (ArgumentOutOfRangeException e)
            {
                throw new SerialPortException($"[{DeviceType}]: One or more of the properties for this instance are invalid, check Parity, DataBits, Handshake and etc.", e);
            }
            catch (ArgumentException e)
            {
                throw new SerialPortException($"[{DeviceType}]: The port name does not begin with 'COM'. - or -The file type of the port is not supported.", e);
            }
            catch (IOException e)
            {
                throw new SerialPortException($"[{DeviceType}]: The port is in an invalid state.", e);
            }
            catch (InvalidOperationException e)
            {
                throw new SerialPortException($"[{DeviceType}]: The port is already open.", e);
            }
            catch (Exception e)
            {
                throw new SerialPortException($"[{DeviceType}]: Unknown error.", e);
            }
        }

        protected bool Write(byte[] command)
        {
            Debug.Assert(null != command && command.Length > 0);

            if (!IsSerialPortOpen)
            {
                throw new InvalidOperationException($"[{DeviceType}]: Serial Port is not open.");
            }

            SerialPort.Write(command, 0, command.Length);
            return true;
        }

        protected bool Read(byte[] buffer, int offset, int count)
        {
            int bytesAlreadyRead = offset;
            int bytesToRead = count;

            try
            {
                while (bytesAlreadyRead < (offset + count))
                {
                    var readOnce = SerialPort.Read(buffer, bytesAlreadyRead, bytesToRead);
                    bytesToRead -= readOnce;
                    bytesAlreadyRead += readOnce;
                }
                return true;
            }
            catch (Exception ex)
            {
                Log.Error($"{DeviceType}: ex.Message", ex);
                return false;
            }
        }

        protected string ReadLine()
        {
            return SerialPort.ReadLine();
        }
    }
}
